#include "../../include/ui/mainwindow.h"
#include "../../include/utils.h"
#include "../../include/file.h"

#include <bits/types/wint_t.h>
#include <bits/stdc++.h>
#include <ncurses.h>
#include <curses.h>
#include <string>

#define SCREEN_MAIN 0

#define KKEY_DOWN 258
#define KKEY_UP 259
#define KKEY_ESC 27
#define KEY_q 113

void MainWindow::moveUp() { 
	this->currentItem--;
	if (this->currentItem < 0) this->currentItem = 0;
}

void MainWindow::moveDown() { 
	this->currentItem++;
	int size = this->currentItems.size()-1;
	if (this->currentItem > size) this->currentItem = size;
}

vector<Entity*> MainWindow::getVisibleItems() {
	int start   = this->scrollTop;
	int end     = this->scrollTop + this->maxRows;
	int cline   = this->currentItem + 1;

	int sdelim  = this->indent - (cline - start - 1);
	int edelim  = this->indent - (end - cline);

	if (sdelim > 0) {
        start   -= sdelim;
        end     -= sdelim;
    }
	if (edelim > 0) {
        end     += edelim;
        start   += edelim;
    }

	if (start < 0) {
        start   = 0; 
        end     = this->maxRows;
    }

	this->currentItems.clear();
    for (int i=0; i <= this->menuItems.size()-1; i++) {
        Entity* entity = this->menuItems[i];

        string name = entity->getName();
        string itemName = lower(name);

        char delim = ' '; vector<string> out;
        split(this->searchQuery, delim, out);

        if (itemName.find(this->searchQuery) != string::npos
            ||
            itemName.find(out[0]) != string::npos
        ) {
            if (this->showHidden || itemName.at(0) != '.') {
                this->currentItems.push_back(this->menuItems[i]);
            }
        }
    }

	int isize = this->currentItems.size()-1;
	if (end > isize) {
		end = isize; start = end - this->maxRows;
	}

	if (start < 0) {start = 0;}

    vector<Entity*> items;
    if (!this->currentItems.empty()) {
	    for (int i=start; i <= end; i++) {
            items.push_back(this->currentItems[i]);
        }
    }

	this->scrollTop = start;

	return items;
}

void MainWindow::toggleSearchMode() {
    if (this->curMode == m_search)
        this->curMode = m_std;
    else
        this->curMode = m_search;
}

void MainWindow::turnStdMode() {
    this->curMode = m_std;
}

void MainWindow::toggleCmdMode() {
    if (this->curMode == m_cmd)
        this->curMode = m_std;
    else
        this->curMode = m_cmd;
}

Entity* MainWindow::searchKeys() {
    if (this->lastKey == KEY_UP) {this->moveUp(); return nullptr;}
    if (this->lastKey == KEY_DOWN) {this->moveDown(); return nullptr;}

    if (this->lastKey == KEY_ENTER || this->lastKey == '\n' || this->lastKey == '\r') {
        return this->currentItems[this->currentItem];
    } else if (this->lastKey == KKEY_ESC) {
        this->turnStdMode();
    }
/*
    else if (this->lastKey == '\t') {
        this->searchQuery = this->currentItems[this->currentItem];
    }
*/
    else if (this->lastKey == KEY_BACKSPACE) {
        if (!this->searchQuery.empty()) {
            this->searchQuery.pop_back();
        }
    } else if (lastKey == KEY_STAB || lastKey == '\t') {
        this->completion();
    } else {
        this->currentItem = 0;
        this->searchQuery.push_back(this->lastKey);
    }

    return nullptr;
}

Entity* MainWindow::cmdKeys() {
    if (this->lastKey == KEY_ENTER || this->lastKey == '\n' || this->lastKey == '\r') {
        this->cmd.launchCommand(this->cmdLine, this->currentItems[this->currentItem]->path);
        this->saveCommandToHistory();
        this->cmdLine.clear();
        this->turnStdMode();
        return new Dir(this->currentDirPath);
    } else if (this->lastKey == KKEY_ESC) {
        this->cmdLine.clear();
        this->turnStdMode();
    } else if (this->lastKey == KEY_BACKSPACE) {
        if (!this->cmdLine.empty()) {
            this->cmdLine.pop_back();
        }
    } else if (lastKey == KEY_STAB || lastKey == '\t') {
        this->completion();
    } else if (lastKey == KKEY_DOWN) {
        this->moveHistoryDown();
    } else if (lastKey == KKEY_UP) {
        this->moveHistoryUp();
    } else {
        this->cmdLine.push_back(this->lastKey);
    }

    return nullptr;
}

Entity* MainWindow::defaultKeys() {
    if (lastKey == KEY_ENTER || lastKey == 'l' 
            || 
        lastKey == '\n' || this->lastKey == '\r' 
    ) {
        return this->currentItems[this->currentItem];
    }

    if (lastKey == '/') 
        this->toggleSearchMode();

    if (lastKey == ':') 
        this->toggleCmdMode();

    if (lastKey == '.') {
        if (showHidden)
            showHidden = false;
        else 
            showHidden = true;
    }

    if (lastKey == 'd') {
        this->cmd.launchCommand("rm", this->currentItems[this->currentItem]->path);
        return new Dir(this->currentDirPath);
    }

    if (lastKey == 's') {
        this->curMode = m_cmd;
        this->cmdLine = "sh ";
    }

    if (lastKey == KEY_BACKSPACE || lastKey == 'h') {
        return new Dir(libfm::getDirname(this->currentDirPath));
    }

    if (lastKey == KEY_UP || lastKey == 'k') this->moveUp();
    if (lastKey == KEY_DOWN || lastKey == 'j') this->moveDown();

    if (lastKey == KEY_q) {this->terminated = true;}

    return nullptr; 
}

void MainWindow::inputKey() {
    wint_t ch; get_wch(&ch);
    this->lastKey = ch;
}

void MainWindow::updateScrollTop() {
	int currentLine = this->currentItem + 1;
	if (currentLine <= this->scrollTop) 
        this->scrollTop = currentLine - 1;
	else if (currentLine - this->scrollTop > this->maxRows)
		this->scrollTop = currentLine - this->maxRows;
}

void MainWindow::completion() {
    string name = this->currentItems[this->currentItem]->getName();
    if (curMode == m_search) 
        this->searchQuery += name;
    if (curMode == m_cmd)
        this->cmdLine += name;
}

Entity* MainWindow::process() {
	while (!terminated) {
		getmaxyx(this->win, this->maxY, this->maxX);
		this->maxRows = this->maxY - 3;

	    switch (this->screen) {
			default: this->drawMainScreen();
		}
       
        this->inputKey();

        if (curMode == m_search) {
            Entity* result = this->searchKeys();
            if (result) {return result;}
        } else if (curMode == m_cmd) {
            Entity* result = this->cmdKeys();
            if (result) {
                Dir* dir = dynamic_cast<Dir*>(result);
                if (dir) 
                    return result;
            }
        } else {
            Entity* result = this->defaultKeys();
            if (result) {
                File* file = dynamic_cast<File*>(result);
                if (file)
                    file->open();
                else
                    return result;
            }
        }

        switch (this->screen) {
            default: this->drawMainScreen();
        }

		usleep(1000);
	}

	return nullptr; 
}

void MainWindow::drawHeader() {
    int startX = 0;
    int startY = 0;

    string updatedPath = this->currentDirPath;
    replace(updatedPath, libfm::getHome(), "~");

    mvaddstr(startY, startX, updatedPath.c_str());
}

void MainWindow::drawSearchedItem(int startY, int startX, string &name) {
    string fullstr          = name;
    size_t found            = fullstr.find(this->searchQuery);
    int queryLength         = this->searchQuery.length();
    string beforeQueryPart  = fullstr.substr(0, found);
    string queryPart        = fullstr.substr(found, queryLength);
    string afterQueryPart   = fullstr.substr(found + queryLength, fullstr.length());

    mvaddstr(startY, startX, beforeQueryPart.c_str());
    attron(A_BOLD);
    mvaddstr(startY, startX+beforeQueryPart.length(), queryPart.c_str());
    attroff(A_BOLD);
    mvaddstr(startY, startX+beforeQueryPart.length()+queryLength, afterQueryPart.c_str());
}

void MainWindow::drawBody() {
	attrset(COLOR_PAIR(1));

    int startX = 2;
    int startY = 1;
    int endX = this->maxX / 2;

    this->updateScrollTop();
	this->visibleItems = this->getVisibleItems();
	int index = this->scrollTop;

	for (const auto &item : this->visibleItems) {
		if (this->currentItem == index) 
            attrset(COLOR_PAIR(2));
		else 
            attrset(COLOR_PAIR(1));

		this->clearLine(startY, endX);

        if (this->currentItem == index)
            mvaddstr(startY, 0, string(">").c_str());
        
        Dir *dir = dynamic_cast<Dir*>(item);
        if (dir) {
            if (this->currentItem == index)
                attrset(A_BOLD | COLOR_PAIR(2));
            else
                attrset(A_BOLD | COLOR_PAIR(5));
        }

        string name = item->getName();
        size_t found = name.find(this->searchQuery);
        if (found != string::npos && this->searchQuery != "") {
            this->drawSearchedItem(startY, startX, name);
        } else { 
		    mvaddstr(startY, startX, name.c_str());
        }
        index++; startY++;
	}
}

void MainWindow::drawStatus() {
	attrset(A_BOLD | COLOR_PAIR(4));

    int startX = 0;
    int startY = this->maxY - 2;
    int lengthX = this->maxX;

	this->clearLine(startY, lengthX);

    string indexText = to_string(this->currentItem+1);
    string lengthText = to_string(this->currentItems.size());
    string sizeText = "";

    File* file = dynamic_cast<File*>(this->currentItems[this->currentItem]);
    if (file) { 
        auto [size, type] = libfm::getFileSize(file->path);
        sizeText = to_string(size) + type;
    }
    string additionalText = indexText + "/" + lengthText + " " + sizeText;

    string totalText = this->statusText + additionalText;

	mvaddstr(startY, startX, totalText.c_str());
}

void MainWindow::drawSearch() {
	attrset(COLOR_PAIR(1));

    if (this->curMode != m_search) return;
    string text = "/" + this->searchQuery;
    mvaddstr(this->maxY-1, 0, text.c_str()); 
}

void MainWindow::drawCmd() {
	attrset(COLOR_PAIR(1));

    if (this->curMode != m_cmd) return;
    string text = ":" + this->cmdLine;
    mvaddstr(this->maxY-1, 0, text.c_str()); 
}

void MainWindow::drawDirPreview(Dir* dir) {
    int startX = this->maxX / 2;
    int startY = 1;
    
    char delim = '\n'; vector<string> out;
    vector<Entity*> items = dir->getItems();

    for (auto item : items) {
        attrset(COLOR_PAIR(1));

        Dir* dir = dynamic_cast<Dir*>(item);
        if (dir) {
            attrset(A_BOLD | COLOR_PAIR(5));
        }
        mvaddstr(startY, startX, item->getName().c_str());
        startY++;
    }
}

void MainWindow::drawFilePreview(File* file) {
    int startX = this->maxX / 2;
    int startY = 1;
   
    if (!libfm::isFileBin(file->path)) {
        char delim = '\n'; vector<string> out;
        string text = file->getInsides();
        split(text, delim, out);

        for (auto line : out) {
            string croppedLine = cropLine(line, startX);
            mvaddstr(startY, startX, croppedLine.c_str());
            startY++;
        }
    }
}

void MainWindow::drawPreview() {
    attrset(COLOR_PAIR(1));

    File* file = dynamic_cast<File*>(this->currentItems[this->currentItem]);
    Dir* dir = dynamic_cast<Dir*>(this->currentItems[this->currentItem]);
    if (file) {
        this->drawFilePreview(file);
    } else if (dir) {
        this->drawDirPreview(dir);
    }
}

void MainWindow::drawMainScreen() {
	attrset(COLOR_PAIR(1));
	for (int i = 0; i <= this->maxY; i++) this->clearLine(i, this->maxX);

    this->drawHeader();
	this->drawBody();
	this->drawStatus();
    this->drawSearch();
    this->drawCmd();
    this->drawPreview();

	curs_set(0);
	refresh();
}

void MainWindow::clearLine(int startY, int length) {
    // This method clear line from another symbols

	move(startY, 1); length++;
	char str[length];
	for (int i = 0; i < length; i++) str[i] = ' ';
	str[length-1] = '\0';
	mvaddstr(startY, 0, str);
}

void MainWindow::initTerm() {
    /*
     This method needs for normal redirecting stdout from ncurses apps
    */

    FILE* tty = fopen("/dev/tty", "r+");
    SCREEN* screen = newterm(NULL, tty, tty);
    set_term(screen);    
}

void MainWindow::initColors() {
    // This method initialize terminal colors and color pairs

	if (has_colors()) {
		start_color();
        use_default_colors();
		init_pair(1, COLOR_WHITE, -1);
		init_pair(2, COLOR_GREEN, COLOR_BLUE);
		init_pair(3, COLOR_BLACK, COLOR_BLUE);
        init_pair(4, COLOR_GREEN, -1);
        init_pair(5, COLOR_BLUE, -1);
	} 
}

void MainWindow::initWindow() {
    // This method initialize win and some ncurses settings

    setlocale(LC_ALL, "");

    this->initTerm();

	keypad(stdscr, true);
	nonl();
	cbreak();
	noecho();
    set_escdelay(25);

	this->win = newwin(0, 0, 0, 0);
    this->initColors();
    this->loadHistory();
}

void MainWindow::saveCommandToHistory() {
    string path = libfm::getHome() + "/.freeda_history";
    history.push_back(cmdLine);
    libfm::appendTextToFile(path, cmdLine + "\n");
}

void MainWindow::loadHistory() {
    history.clear();
    string path = libfm::getHome() + "/.freeda_history";
    string rawHistory = libfm::readFile(path);
    
    vector<string> out;
    split(rawHistory, '\n', out);
    
    for (auto &line : out)
       history.push_back(line);
    historyIndex = out.size();
}

Entity* MainWindow::start() {
    // This method starts window

    Entity* result = this->process();
    return result;
}

MainWindow::MainWindow(vector<Entity*> &items) {
    // This method copy items to this->menuItems vector

	for (const auto &item : items) {
		this->menuItems.push_back(item);
	} 
    this->currentItems = this->menuItems;
	this->lastItem = this->menuItems.size() - 1;
}




